import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.LinkedList;
import java.util.Scanner;

/*FOR THE PURPOSE OF THIS PROJECT, WE WILL BE USING INTEGERS TO MANAGE
 * TIME IN EVERY ASPECT OF THIS PROJECT AND THE INTEGERS WILL BE
 * .1 MSEC OR A TENTH OF A MSEC. HOWEVER, THE OUTPUT FOR THE STATISTICS
 * OF THIS SIMULATION WILL BE CONVERTED TO DOUBLES AND DISPLAYED AS MSEC.
 * THE REASON FOR USING INTS IS BECAUSE OF PRECISION ERRORS OF DOUBLES.
 */

public class Simulator {

	private ProcessQueue[] roundRobin;
	private ProcessQueue eventQueue;
	private ProcessQueue finishedJobs;
	private Device cpu;
	private Device p1;
	private Device p2;
	private Device dd;
	private int clock;
	private Scanner stdin;
	private boolean useAging = false;
	private boolean useTrace = false;
	private boolean usePause = false;
	private boolean useIntervalPause = false;
	private int pauseTime, intervalPauseTime, nextIntervalPauseTime;

	public Simulator() {
		// initialize the roundRobin queues
		clock = 0;
		roundRobin = new ProcessQueue[4];

		roundRobin[0] = new ProcessQueue(80);
		roundRobin[1] = new ProcessQueue(160);
		roundRobin[2] = new ProcessQueue(300);
		roundRobin[3] = new ProcessQueue(500);
		eventQueue = new ProcessQueue(); // to read from input file and store
		// events
		finishedJobs = new ProcessQueue();

		p1 = new Device();
		p2 = new Device();
		dd = new Device();
		cpu = new Device();

		stdin = new Scanner(System.in);
	}

	public void agingQuestion() {
		System.out.println("Use aging? (yes or no): ");

		if ((stdin.next()).charAt(0) == 'y')
			useAging = true;
	}

	public void traceQuestion() {
		System.out.println("Use trace? (yes or no): ");

		if ((stdin.next()).charAt(0) == 'y')
			useTrace = true;
	}

	public void intervalPauseQuestion() {
		System.out
				.println("Display queues after a certain interval? (yes or no): ");

		if ((stdin.next()).charAt(0) == 'y') {
			useIntervalPause = true;

			System.out.println("Enter a time in msec: ");
			intervalPauseTime = stdin.nextInt() * 10;
			nextIntervalPauseTime = intervalPauseTime;
		}
	}

	public void pauseTimeQuestion() {
		System.out.println("Pause the simulation after a time? (yes or no): ");

		if ((stdin.next()).charAt(0) == 'y') {
			usePause = true;

			System.out.println("Enter a time in msec: ");
			pauseTime = stdin.nextInt() * 10;
		}
	}

	public void displayQueues() {

		System.out.print(clock / 10 + "." + clock % 10
				+ "\tQueues contents (remaining CPU time)");
		for (int i = 0; i < 4; i++) {
			System.out.print("\n\tQ" + (i + 1) + ": ");
			if (roundRobin[i].peek() != null) {
				for (Job j : roundRobin[i]) {
					if (roundRobin[i].indexOf(j) != 0) {
						System.out.print(", ");
					}
					System.out.print(j.getJobNumber() + "(" + j.getCpuTime()
							/ 10 + "." + j.getCpuTime() % 10 + ")");
				}
			} else {
				System.out.print("empty");
			}
		}

		System.out.print("\n\tDisk Q: ");

		if (dd.getDeviceQueue().peek() != null) {
			for (Job j : dd.getDeviceQueue()) {
				if (dd.getDeviceQueue().indexOf(j) != 0) {
					System.out.print(", ");
				}
				System.out.print(j.getJobNumber() + "(" + j.getCpuTime() / 10
						+ "." + j.getCpuTime() % 10 + ")");
			}
		} else {
			System.out.print("empty");
		}

		System.out.print("\n\tPrinter1 Q: ");

		if (p1.getDeviceQueue().peek() != null) {
			for (Job j : p1.getDeviceQueue()) {
				if (p1.getDeviceQueue().indexOf(j) != 0) {
					System.out.print(", ");
				}
				System.out.print(j.getJobNumber() + "(" + j.getCpuTime() / 10
						+ "." + j.getCpuTime() % 10 + ")");
			}
		} else {
			System.out.print("empty");
		}
		System.out.print("\n\tPrinter2 Q: ");

		if (p2.getDeviceQueue().peek() != null) {
			for (Job j : p2.getDeviceQueue()) {
				if (p2.getDeviceQueue().indexOf(j) != 0) {
					System.out.print(", ");
				}
				System.out.print(j.getJobNumber() + "(" + j.getCpuTime() / 10
						+ "." + j.getCpuTime() % 10 + ")");
			}
		} else {
			System.out.print("empty");
		}

		System.out.println("");
	}

	public void readFile() {
		System.out.println("Enter name of file: ");
		String fileName = stdin.nextLine();
		String fileLine;
		Job job;

		try {

			File file = new File(fileName);
			Scanner fileIn = new Scanner(file);
			Scanner jobAttr;

			while (fileIn.hasNextLine()) {
				fileLine = fileIn.nextLine();

				// skip empty lines or lines with space
				while (fileLine.isEmpty() || fileLine.charAt(0) == ' ') {
					if (!fileIn.hasNextLine())
						return;
					else
						fileLine = fileIn.nextLine();
				}

				jobAttr = new Scanner(fileLine);
				job = new Job();

				// reading the tokens of each line and setting the job
				// attributes
				job.setJobNumber(jobAttr.nextInt());

				job.setArrTime(jobAttr.nextInt() * 10); // since number read in
				// is msec and we use .1
				// msec
				job.setCpuTime(jobAttr.nextInt() * 10);
				job.setProbdd(jobAttr.nextDouble());
				job.setDdavg(jobAttr.nextInt() * 10);
				job.setProbpr1(jobAttr.nextDouble());
				job.setPr1avg(jobAttr.nextInt() * 10);
				job.setProbpr2(jobAttr.nextDouble());
				job.setPr2avg(jobAttr.nextInt() * 10);
				job.setPriority(jobAttr.nextInt());

				// load job to eventQueue
				eventQueue.add(job);

			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
	}

	public void displayStats() {
		// compute and output the stats, don't forget to convert to msec by
		// dividing by 10 to a double
		double avgWaitTime = 0, avgRespTime = 0, avgTurnaround = 0;
		Job tmp;
		int size = finishedJobs.size();

		while (!finishedJobs.isEmpty()) {
			tmp = finishedJobs.poll();
			avgWaitTime += tmp.getWaitTime();
			avgRespTime += tmp.getResponseTime();
			avgTurnaround += tmp.getTurnAroundTime();
		}
		System.out.println("Number of Jobs: " + size);
		System.out.println("Average Wait Time: " + avgWaitTime / 10 / size
				+ " msecs");
		System.out.println("Average Response Time: " + avgRespTime / 10 / size
				+ " msecs");
		System.out.println("Average Turnaround Time: " + avgTurnaround / 10
				/ size + " msecs");
	}

	public void addCPUJob() {
		int i;
		cpu.setCurrProcess(null); // set cpu process to null because RR queues
		// might be empty

		// only remove from RR if RR queues above are empty
		for (i = 0; i < 4; i++) {
			if (roundRobin[i].peek() != null) {
				cpu.setCurrProcess(roundRobin[i].poll());
				switch (i) {
				case 0:
					cpu.getCurrProcess().setQuantaLimit(8 * 10);
					break;
				case 1:
					cpu.getCurrProcess().setQuantaLimit(16 * 10);
					break;
				case 2:
					cpu.getCurrProcess().setQuantaLimit(30 * 10);
					break;
				case 3:
					cpu.getCurrProcess().setQuantaLimit(50 * 10);
				}
				break;
			}
		}

		if (cpu.getCurrProcess() != null) { // new job found and sent to CPU,
			// add the wait time in queue to
			// job's total waiting time
			cpu.getCurrProcess().addWaitTime(clock);
			// check for first time getting CPU (for response time)
			if (useTrace)
				System.out.println(clock / 10 + "." + clock % 10 + "\tProcess "
						+ cpu.getCurrProcess().getJobNumber()
						+ " beginning CPU burst time");
			if (cpu.getCurrProcess().isFirstTime()) {
				cpu.getCurrProcess().setResponseTime(
						clock - cpu.getCurrProcess().getArrTime());
				cpu.getCurrProcess().setFirstTime(false);
			}
		}
	}

	public void checkAging() {
		int size, i, j;
		Job tmp;

		for (i = 1; i < 4; i++) {
			size = roundRobin[i].size();
			for (j = 0; j < size; j++) {
				// check every elemenet in the roundRobin queue
				tmp = roundRobin[i].pop();
				// System.out.println("Process " + i + "Popped");
				// if waitTime in queue has exceeded 5*its own quanta time
				if ((clock - tmp.getAgingStartTime()) >= (roundRobin[i]
						.getQuanta() * 5)) {
					tmp.setPriority(tmp.getPriority() - 1);
					tmp.setAgingStartTime(clock); // reset aging clock

					if (useTrace) {
						System.out.println(clock / 10 + "." + clock % 10
								+ "\tProcess " + tmp.getJobNumber()
								+ " aged, priority increased by 1");
					}
					// if new priority allows job to move up round robin queue
					if (tmp.getPriority() / 4 < i) {
						roundRobin[i - 1].add(tmp);
						if (useTrace) {
							System.out.println(clock / 10 + "." + clock % 10
									+ "\tProcess" + tmp.getJobNumber()
									+ " has been moved to Q" + (i - 1)
									+ " due to aging");
						}
					} else
						roundRobin[i].add(tmp);
				} else
					roundRobin[i].add(tmp);

			}
		}
	}

	public void simulate() {

		Job tmp = null; // used to make moving jobs easier
		int size = eventQueue.size();

		while (finishedJobs.size() < size) { // keep looping finished jobs < #
			// of jobs loaded
			if (useAging) {
				checkAging();
			}

			// determine if any IO operation has terminated
			if ((p1.getCurrProcess() != null)
					&& ((tmp = p1.getCurrProcess()).getPr1Time() == 0)) {
				// add process that is done with print operation back to round
				// robin
				roundRobin[tmp.getPriority() / 4].add(tmp);

				if (useTrace) {
					System.out.println(clock / 10 + "." + clock % 10
							+ "\tPrinter1 operation terminated for Process "
							+ tmp.getJobNumber());
				}

				tmp.setBeginQueueTime(clock); // store time of entering queue
				tmp.setAgingStartTime(clock);
				// set inProcess to null or next job in device queue
				p1.setCurrProcess(p1.getDeviceQueue().poll());

			}

			if ((p2.getCurrProcess() != null)
					&& ((tmp = p2.getCurrProcess()).getPr2Time() == 0)) {
				// add process that is done with print operation back to round
				// robin
				roundRobin[tmp.getPriority() / 4].add(tmp);

				if (useTrace) {
					System.out.println(clock / 10 + "." + clock % 10
							+ "\tPrinter operation terminated for Process "
							+ tmp.getJobNumber());
				}

				tmp.setBeginQueueTime(clock); // store time of entering queue
				tmp.setAgingStartTime(clock);
				// set inProcess to null or next job in device queue
				p2.setCurrProcess(p2.getDeviceQueue().poll());

			}

			if ((dd.getCurrProcess() != null)
					&& ((tmp = dd.getCurrProcess()).getDdTime() == 0)) {
				// add process that is done with disk drive operation back to
				// round robin
				roundRobin[tmp.getPriority() / 4].add(tmp);

				if (useTrace) {
					System.out.println(clock / 10 + "." + clock % 10
							+ "\tDisk transfer terminated for Process "
							+ tmp.getJobNumber());
				}

				tmp.setBeginQueueTime(clock); // store time of entering queue
				tmp.setAgingStartTime(clock);
				// set inProcess to null or next job in device queue
				dd.setCurrProcess(dd.getDeviceQueue().poll());

			}

			// need code to check for CPU "currProcess" and replace with next
			// job in RR queues
			if (cpu.getCurrProcess() != null) {
				if ((tmp = cpu.getCurrProcess()).getCpuTime() == 0) {
					// job is finished
					finishedJobs.add(tmp);
					if (useTrace)
						System.out.println(clock / 10 + "." + clock % 10
								+ "\tProcess " + tmp.getJobNumber()
								+ " has been completed");

					tmp.setTurnAroundTime(clock - tmp.getArrTime()); // store
					// turnaround
					// time
					addCPUJob();
				} else if (tmp.getQuantaLimit() == 0) {
					// job ran out of time according to the quanta of the RR
					// queue they came from
					if (tmp.getPriority() < 12)
						tmp.setPriority(tmp.getPriority() + 4);

					if (useTrace)
						System.out
								.println(clock
										/ 10
										+ "."
										+ clock
										% 10
										+ "\tProcess "
										+ tmp.getJobNumber()
										+ " has exceeded quanta limit, moving back to Q"
										+ (tmp.getPriority() / 4));
					roundRobin[tmp.getPriority() / 4].add(tmp);
					tmp.setBeginQueueTime(clock);
					tmp.setAgingStartTime(clock);
					addCPUJob();
				}
			} else { // cpu has no process running, check RR for jobs
				addCPUJob();
			}

			// since probability of IO operation is by msec and loop runs .1
			// msec
			// we should only check for IO request when clock % 10 = 0

			if (clock % 10 == 0) {
				if (cpu.getCurrProcess() != null) {
					tmp = cpu.getCurrProcess();

					if (Math.random() <= tmp.getProbdd()) {// send disk request
						tmp
								.setDdTime((int) (2 * Math.random() * tmp
										.getDdavg()));
						if (dd.getCurrProcess() == null
								&& dd.getDeviceQueue().isEmpty()) { // if no
																	// disk
							// process
							// running
							// or
							// waiting
							dd.setCurrProcess(tmp);
							if (useTrace) {
								System.out.println(clock / 10 + "." + clock
										% 10 + "\tProcess "
										+ tmp.getJobNumber()
										+ " has begun disk operation");
							}
						} else {
							dd.getDeviceQueue().add(tmp);
							if (useTrace) {
								System.out.println(clock / 10 + "." + clock
										% 10 + "\tProcess "
										+ tmp.getJobNumber()
										+ " has requested disk operation");
								System.out.println(clock / 10 + "." + clock
										% 10 + "\tProcess "
										+ tmp.getJobNumber()
										+ " has been moved to Hard Disk Queue");
							}
							tmp.setBeginQueueTime(clock); // store time of
							// entering queue
							tmp.setAgingStartTime(clock);
						}
						addCPUJob(); // add new CPU process
					} else if (Math.random() <= tmp.getProbpr1()) { // send pr1
						// request
						tmp.setPr1Time((int) (2 * Math.random() * tmp
								.getPr1avg()));
						if (p1.getCurrProcess() == null
								&& p1.getDeviceQueue().isEmpty()) { // if no
																	// disk
							// process
							// running
							// or
							// waiting
							p1.setCurrProcess(tmp);
							if (useTrace) {
								System.out
										.println(clock
												/ 10
												+ "."
												+ clock
												% 10
												+ "\tPrinter1 Operation started for Process "
												+ tmp.getJobNumber());
							}
						} else {
							p1.getDeviceQueue().add(tmp);
							if (useTrace) {
								System.out.println(clock / 10 + "." + clock
										% 10 + "\tProcess "
										+ tmp.getJobNumber()
										+ " has requested Printer1 operation");
								System.out.println(clock / 10 + "." + clock
										% 10 + "\tProcess "
										+ tmp.getJobNumber()
										+ " has been moved to Printer1 Queue");
							}
							tmp.setBeginQueueTime(clock); // store time of
							// entering queue
							tmp.setAgingStartTime(clock);
						}
						addCPUJob(); // add new CPU process
					} else if (Math.random() <= tmp.getProbpr2()) { // send pr1
						// request
						tmp.setPr2Time((int) (2 * Math.random() * tmp
								.getPr2avg()));
						if (p2.getCurrProcess() == null
								&& p2.getDeviceQueue().isEmpty()) { // if no
																	// disk
							// process
							// running
							// or
							// waiting
							p2.setCurrProcess(tmp);
							if (useTrace)
								System.out
										.println(clock
												/ 10
												+ "."
												+ clock
												% 10
												+ "\tPrinter2 Operation started for Process "
												+ tmp.getJobNumber());
						} else {
							p2.getDeviceQueue().add(tmp);
							if (useTrace) {
								System.out.println(clock / 10 + "." + clock
										% 10 + "\tProcess "
										+ tmp.getJobNumber()
										+ " has requested Printer2 operation");
								System.out.println(clock / 10 + "." + clock
										% 10 + "\tProcess "
										+ tmp.getJobNumber()
										+ " has been moved to Printer2 Queue");
							}
							tmp.setBeginQueueTime(clock); // store time of
							// entering queue
							tmp.setAgingStartTime(clock);
						}
						addCPUJob(); // add new CPU process
					}
				}
			}

			// time deductions
			if ((tmp = cpu.getCurrProcess()) != null) {
				tmp.deductCpu();
				tmp.deductQuanta();
			}
			if (p1.getCurrProcess() != null)
				p1.getCurrProcess().deductPr1();
			if (p2.getCurrProcess() != null)
				p2.getCurrProcess().deductPr2();
			if (dd.getCurrProcess() != null)
				dd.getCurrProcess().deductDD();

			// check event queue - if head's arrival time = clock, add to RR
			// queue if so
			if (eventQueue.peek() != null
					&& eventQueue.peek().getArrTime() == clock) {
				eventQueue.peek().setBeginQueueTime(clock); // set the time when
				// job first enters
				// RR queue
				if (useTrace) {
					System.out.println(clock / 10 + "." + clock % 10
							+ "\tNew Process ("
							+ eventQueue.peek().getJobNumber() + ") enters Q"
							+ (eventQueue.peek().getPriority() / 4));
				}

				eventQueue.peek().setAgingStartTime(clock);
				roundRobin[eventQueue.peek().getPriority() / 4].add(eventQueue
						.poll());

				// if RR queues are empty and there are no CPU processes, add
				// just arrived job
				if (cpu.getCurrProcess() == null)
					addCPUJob();
			}

			if (usePause && clock == pauseTime) {
				displayStats();
				System.out.println("Pause. Continue(y/n)?");

				if ((stdin.next()).charAt(0) == 'n') {
					System.out
							.println("Do you want to display the Queues now(y/n)?");
					if ((stdin.next()).charAt(0) == 'y') {
						displayQueues();
					}
					System.out.println("Do you want to pause again(y/n)?");
					if ((stdin.next()).charAt(0) == 'y') {
						System.out.println("Enter next pause time:");

						pauseTime = stdin.nextInt() * 10;
					} else
						System.exit(0);

				}
			}

			if (useIntervalPause && clock == nextIntervalPauseTime) {
				displayQueues();
				nextIntervalPauseTime = clock + intervalPauseTime;

			}

			clock += 1; // increment the clock

		}

	}

	public static void main(String[] args) throws FileNotFoundException {
		// TODO Auto-generated method stub
		Simulator sim = new Simulator();
		sim.readFile();

		sim.agingQuestion();
		sim.traceQuestion();
		sim.intervalPauseQuestion();
		sim.pauseTimeQuestion();

		if (sim.useTrace) {
			PrintStream traceLog  = null;  
			        
			traceLog = new PrintStream(new FileOutputStream("trace",true)); 
			System.setOut(traceLog);
			
			System.out.println("Aging: " + (sim.useAging ? "On" : "Off")
					+ "\t\tTrace: On");
			System.out.println("Display Queues every:"
					+ "\t"
					+ ((sim.useIntervalPause) ? sim.intervalPauseTime/10 + " msec"
							: "Never"));
			System.out.println("Pause at:" + "\t\t" + sim.pauseTime/10 + " msec");
			System.out.println("\tTrace");
			System.out.println("Time\tEvent");
			System.out.println("(msec)\n");

		}
		sim.simulate();
		sim.displayStats();

	}

}
